package ${domain}.action;

import ${domain}.config.ActionConfig;
import ${domain}.frame.auth.LocalData;
import ${domain}.frame.auth.Token;
import ${domain}.frame.base.BaseRequest;
import ${domain}.frame.base.BaseResponse;
import ${domain}.frame.base.ErrorType;
import ${domain}.frame.base.Screen;
import ${domain}.frame.sse.Sser;
import ${domain}.frame.utils.AESUtil;
import ${domain}.frame.utils.LogUtil;
import ${domain}.frame.utils.MD5Util;
import ${domain}.frame.utils.MapperUtil;
import ${domain}.frame.utils.RequestUtil;
import ${domain}.frame.utils.StringUtil;
import ${domain}.module.wsys.ent.Visitor;
import ${domain}.module.wsys.mgr.LogerrManager;
import ${domain}.module.wsys.mgr.TokensManager;
import ${domain}.module.wsys.mgr.VisitorManager;
import ${domain}.module.wsys.req.TokensBuildRequest;
import ${domain}.module.wsys.req.VisitorFindRequest;
import ${domain}.module.wsys.rsp.TokensBuildResponse;
import ${domain}.module.wsys.rsp.VisitorFindResponse;
import com.fasterxml.jackson.core.TreeNode;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Locale;

/**
 * 全局请求Controller，如果无特殊请求，则不需再增加其他Controller
 * 全局htm后缀入口{@link GlobalController#page(Model, HttpServletRequest, HttpServletResponse)}
 * 全局ajax入口{@link GlobalController#ajax(String, String, String, HttpServletRequest, HttpServletResponse, String, MultipartFile)}
 * 全局消息订阅{@link GlobalController#sse(String)}
 * <p>
 * 说明Request命名规则，驼峰式命名
 * Api#Example#Request ==> 目标#动作#Request
 *
 * @author author
 * @version 0.0.1
 * @since 2017-01-01
 */
@Controller
public class GlobalController {

    @Value("${r'${server.servlet.context-path}'}")
    private String context;
    @Value("${r'${web.url.index}'}")
    private String homePage;
    @Value("${r'${web.url.login}'}")
    private String loginPage;
    @Autowired
    private FreeMarkerViewResolver viewResolver;
    @Autowired
    private VisitorManager visitorManager;

    @RequestMapping("/")
    public String home() {
        Token token = LocalData.getToken();
        if (token == null) {
            return "redirect:" + loginPage;
        } else {
            return "redirect:" + homePage;
        }
    }

    /**
     * 当未明确指定控制器时，走该请求，默认返回对应的layout布局和screen视图
     * 当需要使用layout时，不需要返回值，ViewNameTranslator会处理对应关系
     *
     * @param model
     * @param request
     */
    @RequestMapping({"/**/*.htm"})
    public String page(Model model, HttpServletRequest request, HttpServletResponse response) {
        String servletPath = request.getServletPath();// /**/*.htm
        String layout = "/layout/default";
        String page = RequestUtil.getPage();

        try {
            LocaleResolver localeResolver = (LocaleResolver) request.getAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE);
            Locale locale = localeResolver.resolveLocale(request);

            {//查询screen
                layout = "screen/" + page;
                View view = viewResolver.resolveViewName(layout, locale);
                if (view == null) {
                    response.sendError(HttpServletResponse.SC_NOT_FOUND, "");
                    return null;
                }

                // 尝试执行Screen执行器(服务器渲染)，并返回视图模板
                try {
                    String beanClassName = (ActionConfig.SCREEN_PREFIX + page).toLowerCase();
                    Screen screenExec = LocalData.getApplicationContext().getBean(beanClassName, Screen.class);
                    screenExec.exec(model, request, response);

                    if (response.getStatus() != HttpServletResponse.SC_OK) {
                        response.sendError(response.getStatus(),"");
                        return null;
                    }
                } catch (BeansException e) {

                }
            }

            {//查找layout
                String[] split = page.split("/");

                int lt = split.length;
                while (lt > 0) {

                    StringBuilder sb = new StringBuilder("");
                    sb.append("layout");
                    for (int i = 0; i < lt - 1; i++) {
                        sb.append(File.separator);
                        sb.append(split[i]);
                    }

                    layout = sb.toString() + File.separator + split[split.length - 1];

                    View view = viewResolver.resolveViewName(layout, locale);
                    //无法找到对应layout，使用默认layout
                    if (view == null) {
                        layout = sb.toString() + File.separator + "default";
                        View defaultView = viewResolver.resolveViewName(layout, locale);
                        if (null == defaultView && lt == 1) {
                            System.err.println("can not find layout/default.ftl");
                        } else if (null == defaultView) {
                            lt--;
                        } else {
                            break;
                        }
                    } else {
                        break;
                    }
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        // todo 可在此获取共性数据(也可以在全局拦截器GlobalHandlerInterceptor、拦截器作用域比此更高)，
        // todo 例如用户信息等。其他业务数据在页面渲染后通过Ajax请求
        model.addAttribute("token",LocalData.getToken());
        return layout;
    }

    @RequestMapping("/ajax/{module}/{target}/{method}")
    @ResponseBody
    public Object ajax(
            @PathVariable String module,
            @PathVariable String target,
            @PathVariable String method,
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse,
            @RequestBody(required = false) String data,
            @RequestParam(name = "file", required = false) MultipartFile file) {
        try {
            String beanClassName = (ActionConfig.AJAX_PREFIX + module + "/" + target).toLowerCase();
            Object ajax = LocalData.getApplicationContext().getBean(beanClassName);

            Class ajaxClass = ajax.getClass();

            Method[] methods = ajaxClass.getDeclaredMethods();

            Method methodC = null;
            for (Method meth : methods) {
                if (meth.getName().equals(method)) {
                    methodC = meth;
                }
            }

            if (methodC == null) {
                BaseResponse baseResponse = new BaseResponse();
                baseResponse.addError(ErrorType.BUSINESS_ERROR, "未实现的资源方法!");
                return baseResponse;
            }

            Parameter[] parameters = methodC.getParameters();
            Object[] arg = new Object[parameters.length];

            for (int i = 0; i < parameters.length; i++) {
                Parameter parameter = parameters[i];
                if (parameter.getType() == HttpServletRequest.class) {
                    arg[i] = httpServletRequest;
                } else if (parameter.getType() == HttpServletResponse.class) {
                    arg[i] = httpServletResponse;
                } else if (parameter.getType() == TreeNode.class) {
                    arg[i] = MapperUtil.toTree(data);
                } else if (parameter.getType() == String.class) {
                    arg[i] = data;
                } else if (parameter.getType() == MultipartFile.class) {
                    arg[i] = file;
                } else if (BaseRequest.class.isAssignableFrom(parameter.getType())) {
                    arg[i] = MapperUtil.toJava(data, parameter.getType());
                } else if (parameter.getType() == Token.class) {
                    arg[i] = LocalData.getToken();
                } else {
                    arg[i] = null;
                }
            }
            return methodC.invoke(ajax, arg);
        } catch (BeansException e) {
            e.printStackTrace();
            BaseResponse baseResponse = new BaseResponse();
            baseResponse.addError(ErrorType.BUSINESS_ERROR, "未找到对应的目标!");
            return baseResponse;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            BaseResponse baseResponse = new BaseResponse();
            baseResponse.addError(ErrorType.BUSINESS_ERROR, "私有的资源方法!");
            return baseResponse;
        } catch (InvocationTargetException e) {
            e.getTargetException().printStackTrace();
            BaseResponse baseResponse = new BaseResponse();
            baseResponse.addError(ErrorType.BUSINESS_ERROR, "资源方法执行错误[" + e.getTargetException().getMessage() + "]");
            return baseResponse;
        }
    }

    @RequestMapping(path = "/api/{module}/{target}/{method}", method = RequestMethod.POST)
    @ResponseBody
    public String api(
            @PathVariable String module,
            @PathVariable String target,
            @PathVariable String method,
            @RequestParam(required = false) String appKey,
            @RequestParam(required = false) String sign,
            @RequestParam(required = false) Long timestamp,
            @RequestParam(required = false) String token,
            @RequestParam(required = false) String encryptData,
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse) {
        BaseResponse response = new BaseResponse();
        if (appKey == null) {
            response.addError(ErrorType.BUSINESS_ERROR, "应用码参数[appKey]不存在!");
            return MapperUtil.toJson(response);
        } else if (sign == null) {
            response.addError(ErrorType.BUSINESS_ERROR, "签名参数[sign]不存在!");
            return MapperUtil.toJson(response);
        } else if (timestamp == null) {
            response.addError(ErrorType.BUSINESS_ERROR, "时间戳参数[timestamp]不存在!");
            return MapperUtil.toJson(response);
        }

        String data;
        Visitor visitor;

        // 检索应用码对应的接入对象，未找到或发生错误时，返回空白
        VisitorFindRequest visitorFindRequest = new VisitorFindRequest();
        visitorFindRequest.setAppKey(appKey);
        VisitorFindResponse visitorFindResponse = visitorManager.find(visitorFindRequest, LocalData.getSysToken());
        if (visitorFindResponse.hasError()) {
            return MapperUtil.toJson(visitorFindResponse);
        } else if (visitorFindResponse.getResult().size() == 0) {
            response.addError(ErrorType.BUSINESS_ERROR, "接入信息不存在!");
            return MapperUtil.toJson(response);
        } else {
            visitor = visitorFindResponse.getResult().get(0);
        }

        // 解码
        try {
            data = AESUtil.decrypt2String(encryptData, visitor.getAppSecret());
        } catch (Exception e) {
            response.addError(ErrorType.BUSINESS_ERROR, "解码失败,请确认编码是否正确!");
            return MapperUtil.toJson(response);
        }

        // 验证签名
        String sign_ = MD5Util.encode(visitor.getAppSecret() + data + timestamp);
        if (!sign_.equals(sign)) {
            response.addError(ErrorType.BUSINESS_ERROR, "签名验证失败!");
            return AESUtil.encrypt2Base64(MapperUtil.toJson(response).getBytes(), visitor.getAppSecret());
        }

        // 时效性验证
        long currentTime = System.currentTimeMillis();
        if (currentTime - timestamp > 2 * 60 * 1000) {
            response.addError(ErrorType.BUSINESS_ERROR, "请求过期, 或本地时间错误!");
            return AESUtil.encrypt2Base64(MapperUtil.toJson(response).getBytes(), visitor.getAppSecret());
        }

        // 权限验证
        if (StringUtil.isNotEmpty(token)) {
            TokensManager tokensManager = LocalData.getBean(TokensManager.class);
            TokensBuildRequest tokensBuildRequest = new TokensBuildRequest();
            tokensBuildRequest.setToken(token);
            TokensBuildResponse tokensBuildResponse = tokensManager.build(tokensBuildRequest, LocalData.getSysToken());
            LocalData.setToken(tokensBuildResponse.getToken());
            if (!LocalData.getToken().hasRes(httpServletRequest.getServletPath())) {
                response.addError(ErrorType.BUSINESS_ERROR, "[" + httpServletRequest.getServletPath() + "]未授权的资源!");
                return AESUtil.encrypt2Base64(MapperUtil.toJson(response).getBytes(), visitor.getAppSecret());
            }
        } else {
            String excluded = LocalData.getEnvironment().getProperty("web.url.auth.excluded", "") + ",/api/wsys/User/login";
            if (!excluded.contains(excluded)) {
                response.addError(ErrorType.BUSINESS_ERROR, "请通过登录接口获取Token!");
                return AESUtil.encrypt2Base64(MapperUtil.toJson(response).getBytes(), visitor.getAppSecret());
            }
        }

        // 开始处理业务
        try {
            String beanClassName = (ActionConfig.API_PREFIX + module + "/" + target).toLowerCase();
            Object ajax = LocalData.getApplicationContext().getBean(beanClassName);
            Class ajaxClass = ajax.getClass();
            Method[] methods = ajaxClass.getDeclaredMethods();

            Method methodC = null;
            for (Method meth : methods) {
                if (meth.getName().equals(method)) {
                    methodC = meth;
                }
            }

            if (methodC == null) {
                response.addError(ErrorType.BUSINESS_ERROR, "未找到对应的方法!");
                return AESUtil.encrypt2Base64(MapperUtil.toJson(response).getBytes(), visitor.getAppSecret());
            }

            Parameter[] parameters = methodC.getParameters();
            Object[] arg = new Object[parameters.length];

            for (int i = 0; i < parameters.length; i++) {
                Parameter parameter = parameters[i];
                if (parameter.getType() == HttpServletRequest.class) {
                    arg[i] = httpServletRequest;
                } else if (parameter.getType() == HttpServletResponse.class) {
                    arg[i] = httpServletResponse;
                } else if (BaseRequest.class.isAssignableFrom(parameter.getType())) {
                    arg[i] = MapperUtil.toJava(data, parameter.getType());
                } else if (parameter.getType() == TreeNode.class) {
                    arg[i] = MapperUtil.toTree(data);
                } else if (parameter.getType() == Token.class) {
                    arg[i] = LocalData.getToken();
                } else {
                    arg[i] = null;
                }
            }
            response = (BaseResponse) methodC.invoke(ajax, arg);
        } catch (BeansException e) {
            e.printStackTrace();
            response.addError(ErrorType.BUSINESS_ERROR, "未找到对应的目标!");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            response.addError(ErrorType.BUSINESS_ERROR, "方法执必须公开!");
        } catch (InvocationTargetException e) {
            try {
                LogerrManager logerrManager = LocalData.getBean(LogerrManager.class);
                logerrManager.addErr("系统错误", e.getTargetException().getMessage(), LogUtil.getTrace(e));
            } catch (Exception ignored) {

            }
            LogUtil.dumpException(e.getTargetException());
            e.getTargetException().printStackTrace();
            response.addError(ErrorType.BUSINESS_ERROR, "方法执行错误[" + e.getTargetException().getMessage() + "]");
        }
        return AESUtil.encrypt2Base64(MapperUtil.toJson(response).getBytes(), visitor.getAppSecret());
    }

    @RequestMapping(path = "/msvr/{module}/{target}/{method}", method = RequestMethod.POST)
    @ResponseBody
    public BaseResponse msvr(
            @PathVariable String module,
            @PathVariable String target,
            @PathVariable String method,
            @RequestBody String data,
            @RequestHeader String token) {
        BaseResponse response = new BaseResponse();

        LocalData.setToken(MapperUtil.toJava(token,Token.class));

        // 开始处理业务
        try {
            String beanClassName = (ActionConfig.MSVR_PREFIX + module + "/" + target).toLowerCase();
            Object ajax = LocalData.getApplicationContext().getBean(beanClassName);
            Class ajaxClass = ajax.getClass();
            Method[] methods = ajaxClass.getDeclaredMethods();

            Method methodC = null;
            for (Method meth : methods) {
                if (meth.getName().equals(method)) {
                    methodC = meth;
                }
            }

            if (methodC == null) {
                response.addError(ErrorType.BUSINESS_ERROR, "未找到对应的服务方法!");
                return response;
            }

            Parameter[] parameters = methodC.getParameters();
            Object[] arg = new Object[parameters.length];

            for (int i = 0; i < parameters.length; i++) {
                Parameter parameter = parameters[i];
                if (BaseRequest.class.isAssignableFrom(parameter.getType())) {
                    arg[i] = MapperUtil.toJava(data, parameter.getType());
                } else if (parameter.getType() == Token.class) {
                    arg[i] = LocalData.getToken();
                } else {
                    arg[i] = null;
                }
            }
            response = (BaseResponse) methodC.invoke(ajax, arg);
        } catch (BeansException e) {
            e.printStackTrace();
            response.addError(ErrorType.BUSINESS_ERROR, "未找到对应的目标!");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            response.addError(ErrorType.BUSINESS_ERROR, "方法执必须公开!");
        } catch (InvocationTargetException e) {
            try {
                LogerrManager logerrManager = LocalData.getBean(LogerrManager.class);
                logerrManager.addErr("系统错误", e.getTargetException().getMessage(), LogUtil.getTrace(e));
            } catch (Exception ignored) {

            }
            LogUtil.dumpException(e.getTargetException());
            e.getTargetException().printStackTrace();
            response.addError(ErrorType.BUSINESS_ERROR, "服务方法执行错误[" + e.getTargetException().getMessage() + "]");
        }
        return response;
    }

    /**
     * Sse推送服务，服务器向js推送自定义消息
     * Sse推送{@link Sser#push(String, Object)}
     * Sse全部推送{@link Sser#pushAll(Object)}
     */
    @RequestMapping(value = "/sse/{userId}", produces = "text/event-stream;charset=UTF-8")
    public SseEmitter sse(@PathVariable String userId) {
        return Sser.register(userId, new SseEmitter(10000000L));
    }
}
